NextTick

在 Vue 2.4 之前都是使用的 micro-task(微任务),但是 micro-task(微任务) 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macro-task(宏任务) 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 micro-task(微任务),但在特殊情况下会使用 macro-task(宏任务),比如 v-on。

vue 的 nextTick 机制,以及 2.6 之后的 vue 做的更改是啥,为啥会导致一个 bug 。。。????

  1. 2.6 之前可以正常使用,查看 2.6 与 2.5 实现的差别。他这个变更有些破坏性,原本是 macro-task 的,改成了 micro-task. EventLoop 周期变了
  2. 修改这个实现,只需要将这个 api 换成 setTimeout 0 来实现即可

nextTick 原理分析

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。

在 Vue 2.4 之前都是使用的 micro-task(微任务),但是 micro-task(微任务) 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macro-task(宏任务) 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 micro-task(微任务),但在特殊情况下会使用 macro-task(宏任务),比如 v-on。

对于实现 macro-task(宏任务) ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout

if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else if (
  typeof MessageChannel !== "undefined" &&
  (isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === "[object MessageChannelConstructor]")
) {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = () => {
    port.postMessage(1);
  };
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

nextTick 同时也支持 Promise 的使用,会判断是否实现了 Promise

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  // 将回调函数整合进一个数组中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    if (usemacro - task) {
      macroTimerFunc();
    } else {
      microTimerFunc();
    }
  }
  // 判断是否可以使用 Promise
  // 可以的话给 _resolve 赋值
  // 这样回调函数就能以 promise 的方式调用
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}

nextTick 与异步事件更新队列

实现的优先级

常见的宏任务有 setTimeout、MessageChannel、postMessage、setImmediate 微任务有 MutationObserver 和 Promise.then 以及 node 的 process.nextTick

为了程序的优化和性能提升,我们的最佳选择当然是 Promise 啦,可是呢,Promise 属于 es6 中提出的,部分浏览器可能出现不兼容的情况,所以官方就给了一个优雅降级策略,如果当前浏览器支持 Promise 则使用 Promise,其次就是 MutationObserver,如果以上两个都不支持,就只能搬出我们的 setTimeout 了。

MutationObserver

监视 DOM 变动的接口: 当监视的 DOM 发生变动时 MutationObserver 将收到通知并触发事先设定好的回调函数。源码中通过手动创建调用 createTextNode 函数创建一个 TextNode, 后续修改 dom 的 data 的值使得 dom 变化触发 MutationObserver 构造函数中的回调函数执行。

if (
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  var counter = 1;
  var observer = new MutationObserver(nextTickHandler);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
}

异步更新队列

vue 更新 dom 时是异步执行的。数据变化、更新是在主线程中同步执行的;在侦听到数据变化时,watcher 将数据变更存储到异步队列中,当本次数据变化,即主线成任务执行完毕,异步队列中的任务才会被执行(已去重)。

Vue 的数据为什么频繁变化但只会更新一次

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

另外,关于 waiting 变量,这是很重要的一个标志位,它保证 flushSchedulerQueue 回调($nextTick 中执行)允许被置入 callbacks 一次。 因为 Vue 的事件机制是通过事件队列来调度执行,会等主进程执行空闲后进行调度,所以先会去等待所有的同步代码执行完成之后再去一次更新。这样的性能优势很明显,比如:

现在有这样的一种情况,mounted 的时候 test 的值会被循环执行++1000 次。 每次++时,都会根据响应式触发 setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次++都会直接操作 DOM 更新视图,这是非常消耗性能的。 所以 Vue 实现了一个 queue 队列,在下一个 tick(或者是当前 tick 的微任务阶段)统一执行 queue 中 Watcher 的 run。同时,拥有相同 id 的 Watcher 不会被重复加入到该 queue 中去,所以不会执行 1000 次 Watcher 的 run。最终更新视图只会直接将 test 对的 DOM 的 0 变成 1000。 保证更新视图操作 DOM 的动作是在当前栈执行完以后下一个 tick(或者是当前 tick 的微任务阶段)的时候调用,大大优化了性能。 执行顺序 update -> queueWatcher -> 维护观察者队列(重复 id 的 Watcher 处理) -> waiting 标志位处理(保证需要更新 DOM 或者 Watcher 视图更新的方法 flushSchedulerQueue 只会被推入异步执行的$nextTick 回调数组一次) -> 处理$nextTick(在为微任务或者宏任务中异步更新 DOM)->

Vue 是异步更新 Dom 的,Dom 的更新放在下一个宏任务或者当前宏任务的末尾(微任务)中进行执行

由于 VUE 的数据驱动视图更新是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。在同一事件循环中的数据变化后,DOM 完成更新,立即执行 nextTick(callback)内的回调。 vue 和 react 一样,对 dom 的修改都是异步的。它会在队列里记录你对 dom 的操作并进行 diff 操作,后一个操作会覆盖前一个,然后更新 dom。

异步更新队列

vue 更新 dom 时是异步执行的。数据变化、更新是在主线程中同步执行的;在侦听到数据变化时,watcher 将数据变更存储到异步队列中,当本次数据变化,即主线成任务执行完毕,异步队列中的任务才会被执行(已去重)。

宏事件

  • setImmediate
  • MessageChannel
  • setTimeout 微事件
  • promise

顺序是, promise => setImmediate => MessageChannel => setTimeout

观察对象和观察数组的区别

数组的 api 需要去重写。所以数组一般通过扩展重新赋值,或者使用这些覆盖的 api 变化的时候才可以被更新,否则的话不更新,比如通过下标直接进行更改。

Vue 的更新机制

有一个的事件队列推入,多次推入,会多次更新。 如果在一个作用域中,同时修改一个值,这样会被合成一个更新,推入事件队列中。

上面提到, vue 的更新是 model 中数据的变化引发在初始化时注入的 watcher 的变化,从而引起 view 层的更新.只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。

根据以上特点,我们知道 vue 中的组件更新是有 model 数据的更新引起的,因为 view 和 model 在初始化时已经完成绑定,所以当 model 发生变化时,哪些 view 需要变化已经很明确了,所以就不需要像 React 那般去判断比对了。

Last Updated:
Contributors: yiliang114